package com.lx.boot.oss;

import com.amazonaws.ClientConfiguration;
import com.amazonaws.HttpMethod;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.client.builder.AwsClientBuilder;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.amazonaws.services.s3.model.*;
import com.lx.annotation.Note;
import com.lx.boot.OS;
import com.lx.boot.oss.entity.*;
import com.lx.boot.web.valid.Validate;
import com.lx.entity.ExpireCache;
import com.lx.util.LX;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import java.util.Date;

/**
 * S3 存储协议 所有兼容S3协议的云厂商均支持
 * 阿里云 腾讯云 七牛云 minio
 *
 * @author Lion Li
 */
@Slf4j
public class OssClient {

    private OssProperties properties;

    private final AmazonS3 client;

    @Note("缓存系统配置,提高效率")
    private static ExpireCache<String,OssClient> expireCache = new ExpireCache(128,24L*3600L*1000L);

    @Note("获取默认配置的OSS实例")
    public static OssClient getInstance(){
        return getInstance(OS.getBeanProperty(OssProperties.class));
    }
    @Note("获取指定前缀配置的OSS实例")
    public static OssClient getInstance(String prefix){
        return getInstance(OS.getBeanProperty(OssProperties.class, prefix));
    }
    @Note("获取指定配置文件的OSS实例")
    public static OssClient getInstance(OssProperties ossProperties){
        String md5 = LX.md5(LX.toJSONString(ossProperties));
        return LX.ifNull(expireCache.get(md5),()->{
            OssClient ossClient = new OssClient(ossProperties);
            expireCache.put(md5,ossClient);
            return ossClient;
        });
    }

    @Note("下载文件")
    public InputStream download(String path) {
        log.info("下载文件路径:{}",path);
        path = path.replace(getUrl() + "/", "");
        log.info("实际下载路径:{},bucketName:{}",path,properties.getBucketName());
        S3Object object = client.getObject(properties.getBucketName(), path);
        return object.getObjectContent();
    }

    @Note("下载文件并转成byte数组")
    public byte[] downloadByte(String path) {
        return LX.inputStreamToByteArray(download(path));
    }

    @Note("获取私有文件公共访问路径")
    public String getFileUrl(DownloadUrlDTO dto) {
        LX.validate(dto);
        GeneratePresignedUrlRequest generatePresignedUrlRequest =
                new GeneratePresignedUrlRequest(properties.getBucketName(), dto.getPath())
                        .withMethod(HttpMethod.GET)
                        .withExpiration(new Date(System.currentTimeMillis() + 1000L * dto.getSecond()));
        URL url = client.generatePresignedUrl(generatePresignedUrlRequest);
        return url.toString();
    }

    @Note("上传文件")
    public String upload(UploadDTO dto) {
        LX.validate(dto);
        String fileName = dto.getFileName();
        String suffix = fileName.lastIndexOf(".") != -1 ? StringUtils.substring(fileName, fileName.lastIndexOf(".")):"";
        String path = dto.getPath();
        if (LX.isEmpty(path)){
            path = LX.getDay()+"/" +LX.getTimestamp()+LX.uuid32(10)+suffix;
        }
        String contentType = dto.getContentType();
        if (LX.isEmpty(contentType)){
            contentType = getcontentType(suffix);
        }

        if (LX.isNotEmpty(OS.getFirmId()) && !path.startsWith(OS.getFirmId())){
            path = OS.getFirmId()+"/"+path;
        }
        InputStream inputStream = dto.getInputStream();
        if (!(inputStream instanceof ByteArrayInputStream)) {
            inputStream =  new ByteArrayInputStream(LX.inputStreamToByteArray(inputStream));
        }
        String url = (dto.isPublicAcl()?getUrl() + "/":"") + path;
        try {
            ObjectMetadata metadata = new ObjectMetadata();
            metadata.setContentType(contentType);
            metadata.setContentLength(inputStream.available());
            metadata.setContentDisposition("inline");
            log.info("文件路径:{}",url);
            PutObjectRequest putObjectRequest = new PutObjectRequest(properties.getBucketName(), path, inputStream, metadata);
            if (dto.isPublicAcl()){
                // 设置上传对象的 Acl 为公共读
                putObjectRequest.setCannedAcl(CannedAccessControlList.PublicRead);
            }
            client.putObject(putObjectRequest);
        } catch (Exception e) {
            throw new RuntimeException("上传文件失败，请检查配置信息:[" + e.getMessage() + "]");
        }
        return url;
    }
    /**
     * // 确保使用的HTTP方法与签名URL匹配
     * // 如果后端生成的是PUT签名URL，前端必须使用PUT方法
     * async function uploadToOSS(file) {
     *   const signedUrl = await getSignedUrl(); // 从后端获取签名URL
     *
     *   const response = await fetch(signedUrl, {
     *     method: 'PUT', // 必须与签名URL生成时指定的方法一致
     *     body: file,
     *     headers: {
     *       'Content-Type': 'image/jpg' // 'application/octet-stream'
     *     }
     *   });
     *   // ...处理响应
     * }
     * @param path 文件路径(包含文件名)
     * @param second 链接有效时长
     * @return
     */
    @Note("上传大文件 前端直接上传")
    public UploadUrlVO uploadUrl(UploadUrlDTO dto){
        LX.validate(dto);
        String fileName = dto.getFileName();
        String suffix = fileName.lastIndexOf(".") != -1 ? StringUtils.substring(fileName, fileName.lastIndexOf(".")):"";
        String path = dto.getPath();
        if (LX.isEmpty(path)){
            path = LX.getDay()+"/" +LX.getTimestamp()+LX.uuid32(10)+suffix;
        }
        String contentType = dto.getContentType();
        if (LX.isEmpty(contentType)){
            contentType = getcontentType(suffix);
        }
        if (LX.isNotEmpty(OS.getFirmId()) && !path.startsWith(OS.getFirmId())){
            path = OS.getFirmId()+"/"+path;
        }
        // 有效期
        Date expiration = new Date();
        expiration.setTime(expiration.getTime()+(dto.getSecond()*1000));
        GeneratePresignedUrlRequest generatePresignedUrlRequest =
                new GeneratePresignedUrlRequest(properties.getBucketName(), path)
                        .withMethod(HttpMethod.PUT)
                        .withContentType(contentType)
                        .withExpiration(expiration);
        String url = client.generatePresignedUrl(generatePresignedUrlRequest).toString();
        return new UploadUrlVO(url, path, contentType);
    }

    @Note("修改指定路径文件为公共读")
    public String setObjectPublicRead(String path) {
        // 设置对象ACL为公共读
        client.setObjectAcl(properties.getBucketName(), path, CannedAccessControlList.PublicRead);
        return getUrl()+"/"+path;
    }

    @Note("删除文件")
    private void deletePath(String path) {
        path = path.replace(getUrl() + "/", "");
        try {
            client.deleteObject(properties.getBucketName(), path);
        } catch (Exception e) {
            throw new RuntimeException("删除文件失败，请检查配置信息:[" + e.getMessage() + "]");
        }
    }

    private OssClient(OssProperties ossProperties) {
        Validate.validate(ossProperties);
        this.properties = ossProperties;
        try {
            LX.exMsg(!properties.getEndpoint().startsWith("http"), "访问站点需要指定http协议");
            AwsClientBuilder.EndpointConfiguration endpointConfig = new AwsClientBuilder.EndpointConfiguration(properties.getEndpoint(), properties.getRegion());
            AWSCredentials credentials = new BasicAWSCredentials(properties.getAccessKey(), properties.getSecretKey());
            AWSCredentialsProvider credentialsProvider = new AWSStaticCredentialsProvider(credentials);
            ClientConfiguration clientConfig = new ClientConfiguration();
            AmazonS3ClientBuilder build = AmazonS3Client.builder()
                    .withEndpointConfiguration(endpointConfig)
                    .withClientConfiguration(clientConfig)
                    .withCredentials(credentialsProvider)
                    .disableChunkedEncoding();
            if (properties.isEnablePathStyleAccess()){
                build.enablePathStyleAccess(); // 桶名在域名后子目录时使用
            }
            this.client = build.build();
        } catch (Exception e) {
            log.error("初始化oss失败,请核对配置信息",e);
            throw new RuntimeException("配置错误! 请检查系统配置:[" + e.getMessage() + "]");
        }

        if (!client.doesBucketExistV2(properties.getBucketName())) {
            log.error("Bucket不存在, 请核对配置信息");
            throw new RuntimeException("Bucket不存在, 请核对配置信息");
        }
    }

    private String getUrl() {
        String endpoint = properties.getEndpoint();
        if (properties.isEnablePathStyleAccess()){
            return endpoint+"/"+properties.getBucketName();
        }else{
            return endpoint.replaceFirst("(?<=^https?://)", properties.getBucketName() + ".");
        }
    }

    private ObjectMetadata getObjectMetadata(String path) {
        path = path.replace(getUrl() + "/", "");
        S3Object object = client.getObject(properties.getBucketName(), path);
        return object.getObjectMetadata();
    }

    @Note("桶不存在时进行创建")
    private void createBucket() {
        try {
            String bucketName = properties.getBucketName();
            if (client.doesBucketExistV2(bucketName)) {
                return;
            }
            CreateBucketRequest createBucketRequest = new CreateBucketRequest(bucketName);
            // 默认创建私有的桶
            AccessPolicyType accessPolicy = AccessPolicyType.PRIVATE;
            createBucketRequest.setCannedAcl(accessPolicy.getAcl());
            client.createBucket(createBucketRequest);
            client.setBucketPolicy(bucketName, getPolicy(bucketName, accessPolicy.getPolicyType()));
        } catch (Exception e) {
            log.error("创建Bucket失败, 请核对配置信息",e);
            throw new RuntimeException("创建Bucket失败, 请核对配置信息:[" + e.getMessage() + "]");
        }
    }

    private static String getPolicy(String bucketName, PolicyType policyType) {
        StringBuilder builder = new StringBuilder();
        builder.append("{\n\"Statement\": [\n{\n\"Action\": [\n");
        builder.append(switch (policyType) {
            case WRITE -> "\"s3:GetBucketLocation\",\n\"s3:ListBucketMultipartUploads\"\n";
            case READ_WRITE -> "\"s3:GetBucketLocation\",\n\"s3:ListBucket\",\n\"s3:ListBucketMultipartUploads\"\n";
            default -> "\"s3:GetBucketLocation\"\n";
        });
        builder.append("],\n\"Effect\": \"Allow\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::");
        builder.append(bucketName);
        builder.append("\"\n},\n");
        if (policyType == PolicyType.READ) {
            builder.append("{\n\"Action\": [\n\"s3:ListBucket\"\n],\n\"Effect\": \"Deny\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::");
            builder.append(bucketName);
            builder.append("\"\n},\n");
        }
        builder.append("{\n\"Action\": ");
        builder.append(switch (policyType) {
            case WRITE -> "[\n\"s3:AbortMultipartUpload\",\n\"s3:DeleteObject\",\n\"s3:ListMultipartUploadParts\",\n\"s3:PutObject\"\n],\n";
            case READ_WRITE -> "[\n\"s3:AbortMultipartUpload\",\n\"s3:DeleteObject\",\n\"s3:GetObject\",\n\"s3:ListMultipartUploadParts\",\n\"s3:PutObject\"\n],\n";
            default -> "\"s3:GetObject\",\n";
        });
        builder.append("\"Effect\": \"Allow\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::");
        builder.append(bucketName);
        builder.append("/*\"\n}\n],\n\"Version\": \"2012-10-17\"\n}\n");
        return builder.toString();
    }


    @Note("根据文件后缀获取contentType")
    public static String getcontentType(String filenameExtension) {
        if (LX.isEmpty(filenameExtension)){
            return "application/octet-stream";
        }
        String type = OS.getProperty("server.oss.contentType" + filenameExtension);
        if (LX.isNotEmpty(type)){
            return type;
        }
        filenameExtension = filenameExtension.toLowerCase().trim();
        switch(filenameExtension){
            case ".jpeg":
            case ".jpg":
                return "image/jpg";
            case ".png":
                return "image/png";
            case ".mp3":
                return "audio/mp3";
            case ".mp4":
                return "video/mp4";
            case ".bmp":
                return "image/bmp";
            case ".gif":
                return "image/gif";
            case ".html":
                return "text/html";
            case ".xml":
                return "text/xml";
            case ".txt":
                return "text/plain";
            case ".docx":
            case ".doc":
                return "application/msword";
            case ".pdf":
                return "application/pdf";
            default:
                return "application/octet-stream";
        }
    }
}
